## Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php

"""
World of Warcraft Armory Character utility module.
"""

## Needed for printing some exceptions
import sys, traceback

## Needed to get things from URLs
import urllib2

## Needed for clearing up the gender string.
import re

## Need a dom for the xpath statements to search
from xml.dom.minidom import parse, parseString

## Needed for parsing the XPath expressions, from py-dom-xpath
## http://code.google.com/p/py-dom-xpath/
import xpath


## The global data structure that contains the xpaths for every kind of page
## if the xpath is explicitly defined (as for advanced, and pvp, etc.) This
## also contains xpaths that are used by the pages that are processed by logic
## that uses the text in the page to determine the name part of the name=value
## pair.

## This will be set to whatever child of page_xpaths (below) is a match for the
## page type that we are instanced as.
xpaths = {}

## This holds the explicit xpaths that name specific things in a page. There
## are other kinds of xpath statements or logic that are better off as part of
## the extract_data function. There are some pages that need explicit names
## however, like the top level achievement or statistic pages where we need to
## know what the Nth update was so we can compare it.
page_xpaths = {}

page_xpaths["advanced"] = {
	"Character Name"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/a[1]/text()",
	"Character Realm"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[3]/span[3]/text()[1]",
	"Character Battlegroup"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[3]/span[3]/@data-battlegroup",
	"Character Region"		: "/html/body[1]/div[1]/div[3]/div[2]/a[1]/span[1]/text()[1]",
	"Character Class"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[3]/a[contains(@class,'class')]/text()[1]",
	"Character Level"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[3]/span[1]/strong[1]/text()[1]",
	"Character Race"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[3]/a[1]/text()[1]",
	"Guild Name"			: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[2]/div[2]/a[1]/text()[1]",
	"Achievement Points"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[4]/a[1]/text()[1]",
	"Average Item Level"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[2]/text()[1]",
	"Average Equipped Item Level"	: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/span[1]/text()[1]",
	"Character Health"		: "/html/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[2]/div[3]/div[2]/div[2]/ul[1]/li[1]/span[2]/text()[1]",
}

class ArmoryCharacterPage:
    """
       A World of Warcraft Armory XHTML page
       It can be fetched, validated and parsed, but that's about it.
       Just a template, each subclass of this will have a different
       SUFFIX_URL, and more complex ones may have more complicated logic
       if they are non-leaf pages.
    """

    def __init__(self, character_url, suffix_url):
        self.CHARACTER_URL = character_url
        self.SUFFIX_URL = suffix_url
        self.url = "%s%s" % (self.CHARACTER_URL, self.SUFFIX_URL)
        self.page_xhtml = ""
        self.page_data = {}

    ## The URL that we will be fetching.
    url = ""

    ## The page XHTML content that we will end up parsing.
    page_xhtml = ""

    ## The page object that we will end up with.
    page_data = {}

    def fetch_data(self):
        ## We are not evaluating the content, just our ability
        ## to fetch a page from the URL given to us.
        try:
            # Specify a user agent so the armory sends XML rather than HTML.
            oOpener = urllib2.build_opener()
            oOpener.addheaders = [
            ]

            ## Fetch the URL
            request = urllib2.Request(self.url)
            self.page_xhtml = oOpener.open(request).read()

            ## If there are no errors up to this point, return True
            return True
        except:
            ## If there are errors be descriptive and return False so the calling program
            ## knows there was a failure
            traceback.print_exc(file=sys.stdout)
            return False

        ## We should never get to here, but just in case we do, return False
        ## as we should never presume anything works.
        return False

    def extract_data(self):
        ## If there are any xpaths to load, load them
        if self.SUFFIX_URL in page_xpaths:
            xpaths = page_xpaths[self.SUFFIX_URL]
        else:
            xpaths = {}

        ## If we encounter any errors when parsing this data, any errors at all, return
        ## False. Either we can completely make sense of the page, or we can't.
        try:
            # Parse the response into an etree element, we need to do this regardless of
            # which page we are processing and what we might search for xpath-wise.
            page_DOM = parseString(self.page_xhtml)

            ## If there are xpath statements for this page type run them and store them in page_data
            ## so that later stages might use them for more complex fetching if needed. The advanced
            ## page is an example of that. Some things such as race are used in a later stage to
            ## determine what the faction is.
            for dataName in sorted(xpaths.keys()):
                if xpath.findvalue(xpaths[dataName], page_DOM):
                    self.page_data[dataName] = (xpath.findvalue(xpaths[dataName], page_DOM)).strip()

            ## Pages that tend to be lists of things are usually something we can iterate
            ## over relatively easily versus writing individual xpath statements for every
            ## item. If we have custom logic for the page, we check for that and perform
            ## that logic.

            if ((self.SUFFIX_URL).find('achievement/') > -1):
                ## The achievement data is highly structured data and requires some custom parsing.

                ## Create the data structure where achievements will be stored.
                self.page_data["achievements"] = {}

                ## Find only the elements that have the achievement class
                for achievement_element in (xpath.find('//li[contains(@class,"achievement")]', page_DOM)): 
                    ## If the class attribute also *does not* contain "locked" then the achievement is
                    ## complete and we should print it, otherwise just ignore it.
                    if not xpath.find('contains(@class,"locked")', achievement_element):
                        achievement_id          = (xpath.findvalue('@data-id', achievement_element)).strip()
                        self.page_data["achievements"][achievement_id] = {}

                        self.page_data["achievements"][achievement_id]["name"]        = (xpath.findvalue('p[1]/strong[1]/text()[1]', achievement_element)).strip()
                        self.page_data["achievements"][achievement_id]["description"] = (xpath.findvalue('p[1]/span[1]/text()[1]', achievement_element)).strip()
                        self.page_data["achievements"][achievement_id]["image"]       = (xpath.findvalue('span[1]/img[1]/@src', achievement_element)).strip()

                        ## If this is a "has-sub" type of achievement the xpaths for the last two are slightly different
                        if xpath.find('contains(@class,"has-sub")', achievement_element):
                            self.page_data["achievements"][achievement_id]["points"] = (xpath.findvalue('div[3]/strong[1]/text()[1]', achievement_element)).strip()
                            self.page_data["achievements"][achievement_id]["date"]   = (xpath.findvalue('div[3]/span[1]/text()[1]', achievement_element)).strip()
                        else:
                            self.page_data["achievements"][achievement_id]["points"] = (xpath.findvalue('div[1]/strong[1]/text()[1]', achievement_element)).strip()
                            self.page_data["achievements"][achievement_id]["date"]   = (xpath.findvalue('div[1]/span[1]/text()[1]', achievement_element)).strip()

                return True
               
            if ((self.SUFFIX_URL).find('statistic/') > -1):
                ## There are any number of li elements that contain the "types" of stats in a single page
                ## so we have to iterate through them, and then for each one of those run through the
                ## dl elements and fetch the data.
                for stat_type in (xpath.find('/div/ul[1]/li', page_DOM)): 
                    for stat_element in xpath.find('dl', stat_type):
                        stat_name = xpath.findvalue('dt/text()', stat_element).strip()
                        stat_value = ""
                        if xpath.findvalue('dd/strong/text()', stat_element):
                            stat_value = "%s %s" % (xpath.findvalue('dd/strong/text()', stat_element).strip(), xpath.findvalue('dd/text()[2]', stat_element).strip())
                        if xpath.findvalue('dd/span/text()', stat_element):
                            stat_value = "%s" % (xpath.findvalue('dd/span/text()', stat_element).strip())
                        stat_value = "%s%s" % (stat_value, xpath.findvalue('dd/text()', stat_element).strip())
                        if stat_value == "":
                            print "Missing value for %s" % (stat_name)
                        self.page_data[stat_name] = stat_value

                return True

            if self.SUFFIX_URL == "advanced":
                ## Let's do a bit more work to figure out some of the more complex values

                ## Calculate the faction based on race data.
                horde_races = ['Blood Elf', 'Undead', 'Goblin', 'Orc', 'Tauren', 'Troll']
                if self.page_data["Character Race"] in horde_races:
                        self.page_data["Character Faction"] = "Horde"
                else:
                        self.page_data["Character Faction"] = "Alliance"

                ## Gender, this one is a little wacky. Thanks to Darque from Elder Tribunal for pointing it out:
                ## http://us.battle.net/wow/en/forum/topic/2516991705?page=2#28
                gender_text_string = (xpath.findvalue('/html/head[1]/style[1]//text()', page_DOM)).strip()
                ## Python trickery here the (?s) forces "." to match everything, including newlines
                gender_string = re.sub('(?s).*alt=.*/', '', gender_text_string)
                ## Turn 1-0.jpg"); } into just the 2nd number there.
                ## Get the first element (0) of an array split on "." : "1-0"
                ## Get the 2nd element (1) of the remainder split on "-" : "0"
		gender = (gender_string.split('.')[0]).split('-')[1]
                if gender == "1":
                    self.page_data["Character Gender"] = "Female"
                else:
                    self.page_data["Character Gender"] = "Male"

                return True


            ## If there are no errors up to this point, return True
            return True

        ## If there are errors be descriptive and return False so the calling program
        ## knows there was a failure
        except:
            traceback.print_exc(file=sys.stdout)
            return False

        ## We should never get to here, but just in case we do, return False
        ## as we should never presume anything works.
        return False

    def process_data(self):
        ## For pages where it's just name=value pairs there is nothing to do here. Just
        ## walk the tree of page_data and if we have more than 0 elements return True.
        ## If we don't have more than 0 elements... something went wrong, despite having
        ## no problems processing the xpath in an earlier step.
        if (len(self.page_data)) > 0:
            return True
        else:
            print "ERROR: Processed page data was 0! This should not happen, but it has!"
            return False

        return False

    ## Get to work fetching, extracting and processing the data!
    ## There is only one path to returning True, if every function
    ## we call returns true. Everything else leads to a return of
    ## False so we know something bad happened.
    def get(self):
        if self.fetch_data():
            if self.extract_data():
                if self.process_data():
                    ## We have successfully fetched, extracted and processed data.
                    ## The final result of all of this is stored as attribute(s)
                    ## of this class and accessible from there.
                    return True

        ## The default is to return false so we can only succeed if everything works
        return False

